Skip to content

in-370-외부-api-호출-bulkhead-패턴-사용 -> develop#223

Merged
HanYoonSoo merged 7 commits into
developfrom
in-370-외부-api-호출-bulkhead-패턴-사용
Jul 13, 2026

Hidden character warning

The head ref may contain hidden characters: "in-370-\uc678\ubd80-api-\ud638\ucd9c-bulkhead-\ud328\ud134-\uc0ac\uc6a9"
Merged

in-370-외부-api-호출-bulkhead-패턴-사용 -> develop#223
HanYoonSoo merged 7 commits into
developfrom
in-370-외부-api-호출-bulkhead-패턴-사용

Conversation

@HanYoonSoo

@HanYoonSoo HanYoonSoo commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Issue


comment

  • Resilience4j Bulkhead/RateLimiter를 도입해 YouTube Data API, YouTube Analytics API, OpenAI 호출의 동시 실행과 순간 호출량을 제한했습니다.
  • 공용 VirtualThread executor를 사용해 YouTube Analytics 영상별 조회를 4개 단위로 병렬 처리하고, 채널 기본 분석과 구독자 로그는 상위 섹션에서 병렬 실행하도록 절충했습니다.
  • RestClient connect/read timeout과 computeAvgUploadDays Future timeout을 추가해 외부 API 응답 지연으로 작업이 무기한 대기하지 않도록 했습니다.
  • Resilience4j Spring Boot 3 의존성을 최신 안정 버전으로 올리고, 제한 permit 획득 실패를 외부 API 429 예외로 매핑했습니다.
  • YouTube Analytics 제한값은 동기식 구조 안에서 과도하게 공격적이지 않도록 16 concurrent / 32 per second로 조정했고, OpenAI는 처리량은 유지하되 permit 대기 시간을 1초로 완화했습니다.
  • YouTube Data API 내부 pagination/chunk 루프는 메서드 단위 RateLimiter와 실제 HTTP 호출 단위가 다를 수 있어, 후속 작업에서 HTTP 호출 단위 rate limiting 또는 공통 호출 래퍼 적용을 검토하면 좋겠습니다.
  • 추후 실제 운영 부하와 사용자 경험을 보면서 Bulkhead/RateLimiter 수치, 재시도 여부, 비동기 job 전환까지 대화하며 절충안을 찾으면 좋겠습니다.

Summary by CodeRabbit

  • New Features
    • 유튜브 데이터/애널리틱스/검색과 오픈AI 요청에 동시성 및 속도 제한이 적용되어 트래픽이 많아도 더 안정적으로 동작합니다.
    • 외부 API 호출 실패 시 공통 오류 코드(429: 외부 API 일시 용량 제한)가 일관되게 반환됩니다.
    • 운영용 헬스/모니터링 기능이 추가되었습니다.
  • Bug Fixes
    • 일부 외부 호출이 실패해도 작업 결과가 끊기지 않고 부분 결과가 유지되도록 개선했습니다.
    • 트렌드/채널 분석 처리가 더 빠르게 완료되도록 병렬 처리 방식을 개선했습니다.
  • Tests
    • 외부 API 제한 및 동시성 처리 시나리오 테스트가 보강되었습니다.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

외부 API 클라이언트에 Resilience4j 제한과 fallback 예외 매핑을 적용하고, YouTube 분석 및 업로드 간격 계산을 가상 스레드 기반 비동기 처리로 전환했습니다. 관련 설정과 동시성·예외 처리 테스트도 추가 또는 수정했습니다.

Changes

외부 API 제한 및 병렬 처리

Layer / File(s) Summary
제한 설정과 실행 기반
build.gradle, .../config/*, .../exception/*, .../resources/application.yml
Resilience4j 의존성, 가상 스레드 ExecutorService, 429 예외 매핑, RestClient 타임아웃과 제한 설정을 추가합니다.
외부 API 클라이언트 제한과 fallback
.../client/Youtube*ApiClient.java, .../openai/service/OpenAiService.java
외부 호출에 @Bulkhead@RateLimiter를 적용하고 fallback에서 제한 예외를 변환합니다.
YouTube 분석 비동기 배치 처리
.../YoutubeChannelAnalyticsFetchService.java, .../YoutubeChannelAnalyticsFetchServiceTest.java
분석 및 유지율 조회를 배치별 병렬 실행으로 전환하고 부분 실패와 동시 실행을 테스트합니다.
업로드 간격 병렬 계산
.../BrandCollaborationService.java
플레이리스트별 업로드 간격 계산을 비동기화하고 타임아웃 및 유효 값 필터링을 적용합니다.

VideoServiceTest 호출 방식 변경

Layer / File(s) Summary
UUID 인증 및 단일 인자 호출
src/test/java/.../VideoServiceTest.java
인증을 UUID 기반으로 변경하고 영상 분석 호출을 videoId 단일 인자로 통일합니다.

AnalyticsCalculatorTest 인자 수정

Layer / File(s) Summary
formatTime 인자 타입 수정
src/test/java/.../AnalyticsCalculatorTest.java
formatTime 테스트의 double 리터럴을 정수 리터럴로 변경합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant YoutubeDataApiClient
  participant Resilience4j
  participant ExternalApiLimitExceptionMapper
  Caller->>YoutubeDataApiClient: getYoutubeChannels(...)
  YoutubeDataApiClient->>Resilience4j: Bulkhead/RateLimiter 검사
  Resilience4j-->>YoutubeDataApiClient: 제한 예외
  YoutubeDataApiClient->>ExternalApiLimitExceptionMapper: 예외 변환
  ExternalApiLimitExceptionMapper-->>Caller: ApiException(EXTERNAL_API_RATE_LIMITED)
Loading
sequenceDiagram
  participant Service as YoutubeChannelAnalyticsFetchService
  participant Executor as externalApiExecutor
  participant ApiClient as YoutubeAnalyticsApiClient
  Service->>Executor: 영상 분석 비동기 배치 실행
  Executor->>ApiClient: getYoutubeAnalytics(...)
  ApiClient-->>Executor: 응답 또는 예외
  Executor-->>Service: join 결과
  Service->>Service: 결과 병합
Loading

Possibly related PRs

Suggested reviewers: youcastle03

Poem

토끼가 API 밭을 깡총 뛰며
제한의 울타리를 살펴보네
가상 스레드 바람 타고
분석 결과 함께 달려와
폴백도 안전하게 착지하네 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 외부 API 호출에 Bulkhead 패턴을 적용한다는 मुख्य 변경을 잘 반영하고 있어 관련성이 높습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch in-370-외부-api-호출-bulkhead-패턴-사용

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@HanYoonSoo HanYoonSoo self-assigned this Jul 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java (1)

98-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback 로그 메시지가 API 호출 실패 상황에서 오해의 소지가 있습니다.

youtubeAnalyticsFallback은 Bulkhead/RateLimiter 거부뿐 아니라 API 호출 자체가 실패(4xx/5xx)한 경우에도 호출됩니다. 현재 로그 메시지 "blocked or failed before execution"는 후자의 경우 실제로는 API 호출이 실행되었음에도 "before execution"으로 표현되어 디버깅 시 혼란을 줄 수 있습니다.

🔧 제안: 로그 메시지 개선
     log.warn("YouTube Analytics API call blocked or failed before execution. googleId={} metrics={} dimensions={}",
             googleId, request.formattedMetricsList(), request.dimensions(), throwable);
-    log.warn("YouTube Analytics API call blocked or failed before execution. googleId={} metrics={} dimensions={}",
-            googleId, request.formattedMetricsList(), request.dimensions(), throwable);
+    log.warn("YouTube Analytics API call rejected or failed. googleId={} metrics={} dimensions={}",
+            googleId, request.formattedMetricsList(), request.dimensions(), throwable);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java`
around lines 98 - 107, The fallback log message in youtubeAnalyticsFallback is
misleading because it says the call was “blocked or failed before execution”
even when the API request actually ran and then failed. Update the log text in
YoutubeAnalyticsApiClient.youtubeAnalyticsFallback to clearly distinguish
pre-execution rejections from downstream API failures, while still logging
googleId, request.formattedMetricsList(), request.dimensions(), and the
throwable for debugging.
src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java (1)

613-619: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

chunked 메서드가 YoutubeDataApiClient에도 동일하게 존재하여 중복됩니다.

그래프 컨텍스트에 따르면 YoutubeDataApiClient.java의 613-619번 라인에 동일한 구현의 private chunked 메서드가 있습니다. 두 클래스에서 같은 유틸리티 로직이 중복되고 있으므로 공용 유틸 클래스로 추출을 고려해 보세요.

♻️ 제안: 공용 유틸 클래스로 추출
-    private <T> List<List<T>> chunked(List<T> values, int size) {
-        List<List<T>> chunks = new ArrayList<>();
-        for (int start = 0; start < values.size(); start += size) {
-            chunks.add(values.subList(start, Math.min(values.size(), start + size)));
-        }
-        return chunks;
-    }
// 예: src/main/java/com/example/inflace/global/util/ListUtils.java
public final class ListUtils {
    private ListUtils() {}
    
    public static <T> List<List<T>> chunked(List<T> values, int size) {
        List<List<T>> chunks = new ArrayList<>();
        for (int start = 0; start < values.size(); start += size) {
            chunks.add(values.subList(start, Math.min(values.size(), start + size)));
        }
        return chunks;
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java`
around lines 613 - 619, `chunked` 로직이 `YoutubeChannelAnalyticsFetchService`와
`YoutubeDataApiClient`에 중복되어 있으니, 두 곳의 private 메서드를 공용 유틸로 합치세요. `chunked`를
`ListUtils` 같은 재사용 가능한 클래스의 static 메서드로 추출하고,
`YoutubeChannelAnalyticsFetchService`와 `YoutubeDataApiClient`는 그 공용 메서드를 호출하도록
변경해 중복 구현을 제거하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.gradle`:
- Around line 28-29: Update the resilience4j dependency to the latest stable
release by changing the io.github.resilience4j:resilience4j-spring-boot3 version
from 2.2.0 to 2.4.0 in build.gradle. Keep the existing Spring Boot actuator
dependency unchanged and make sure the version bump is applied wherever the
resilience4j artifact is declared.

In
`@src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java`:
- Around line 275-294: `computeAvgUploadDays` is waiting indefinitely on YouTube
requests, so add an actual response timeout instead of relying on the
rate-limiter setting alone. Update the async flow around
`CompletableFuture.supplyAsync(...)` and `CompletableFuture::join` to use
`orTimeout()` (or equivalent) for the upload-day computation, and configure the
`RestClient` used by `computeUploadDays` with a read timeout so slow HTTP
responses fail fast. Keep the existing exception handling/logging in
`BrandCollaborationService` to surface timeout failures clearly.

In `@src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java`:
- Around line 90-91: The current `@RateLimiter` on YoutubeDataApiClient only
limits each method invocation once, so it does not control the individual API
requests made inside the pagination/chunk loops. Review getMyVideoIds and
getYoutubeVideos and move rate limiting to the actual YouTube API call points
inside those loops, or switch to HTTP-client-level throttling, while keeping
`@Bulkhead` on the outer methods for concurrency control. Ensure the
fallbackMethod wiring still works after refactoring and that each remote call is
counted against the limiter rather than the enclosing method.

---

Nitpick comments:
In
`@src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java`:
- Around line 613-619: `chunked` 로직이 `YoutubeChannelAnalyticsFetchService`와
`YoutubeDataApiClient`에 중복되어 있으니, 두 곳의 private 메서드를 공용 유틸로 합치세요. `chunked`를
`ListUtils` 같은 재사용 가능한 클래스의 static 메서드로 추출하고,
`YoutubeChannelAnalyticsFetchService`와 `YoutubeDataApiClient`는 그 공용 메서드를 호출하도록
변경해 중복 구현을 제거하세요.

In
`@src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java`:
- Around line 98-107: The fallback log message in youtubeAnalyticsFallback is
misleading because it says the call was “blocked or failed before execution”
even when the API request actually ran and then failed. Update the log text in
YoutubeAnalyticsApiClient.youtubeAnalyticsFallback to clearly distinguish
pre-execution rejections from downstream API failures, while still logging
googleId, request.formattedMetricsList(), request.dimensions(), and the
throwable for debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d20eef6f-4ac0-4735-9e4c-49f7b89a4081

📥 Commits

Reviewing files that changed from the base of the PR and between c4690f3 and 4abca99.

📒 Files selected for processing (15)
  • build.gradle
  • src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java
  • src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java
  • src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java
  • src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java
  • src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java
  • src/main/java/com/example/inflace/global/config/ExternalApiExecutorConfig.java
  • src/main/java/com/example/inflace/global/exception/ErrorDefine.java
  • src/main/java/com/example/inflace/global/exception/ExternalApiLimitExceptionMapper.java
  • src/main/java/com/example/inflace/infra/openai/service/OpenAiService.java
  • src/main/resources/application.yml
  • src/test/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchServiceTest.java
  • src/test/java/com/example/inflace/domain/video/service/VideoServiceTest.java
  • src/test/java/com/example/inflace/global/exception/ExternalApiLimitExceptionMapperTest.java
  • src/test/java/com/example/inflace/global/util/AnalyticsCalculatorTest.java

Comment thread build.gradle Outdated
Comment on lines +275 to 294
List<CompletableFuture<Double>> futures = channelMap.values().stream()
.filter(c -> c.contentDetails() != null && c.contentDetails().relatedPlaylists() != null)
.map(c -> c.contentDetails().relatedPlaylists().uploads())
.filter(StringUtils::hasText)
.mapToDouble(playlistId -> {
List<Instant> instants = youtubeDataApiClient
.getRecentUploadDates(playlistId, RECENT_UPLOADS_COUNT)
.stream().map(Instant::parse).sorted().toList();
if (instants.size() < 2) return Double.NaN;
long spanSeconds = instants.getLast().getEpochSecond() - instants.getFirst().getEpochSecond();
return spanSeconds / 86400.0 / (instants.size() - 1);
})
.map(playlistId -> CompletableFuture
.supplyAsync(() -> computeUploadDays(playlistId), externalApiExecutor)
.exceptionally(exception -> {
log.warn("Failed to compute average upload days. playlistId={}", playlistId, exception);
return Double.NaN;
}))
.toList();

OptionalDouble avg = futures.stream()
.map(CompletableFuture::join)
.mapToDouble(Double::doubleValue)
.filter(d -> !Double.isNaN(d) && d > 0)
.average();

return avg.isPresent() ? avg.getAsDouble() : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file list\n'
git ls-files | rg '^src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService\.java$|^src/main/java/.*RestClient.*|^src/main/java/.*YouTube.*|^src/main/java/.*Executor.*|^src/main/java/.*Http.*|^src/main/resources/.*application.*'

printf '\n## outline BrandCollaborationService\n'
ast-grep outline src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java --view expanded || true

printf '\n## relevant snippets\n'
sed -n '1,220p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java
printf '\n--- middle ---\n'
sed -n '220,360p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java

printf '\n## timeout/search terms\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'orTimeout|completeOnTimeout|readTimeout|connectTimeout|timeout|RestClient|WebClient|HttpClient|ExecutorService|externalApiExecutor|computeUploadDays|analyzeTrends' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 25367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## BrandCollaborationService outline\n'
ast-grep outline src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java --view expanded || true

printf '\n## BrandCollaborationService relevant lines\n'
sed -n '240,320p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java

printf '\n## search for timeout and async config\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'orTimeout|completeOnTimeout|readTimeout|connectTimeout|timeout|externalApiExecutor|computeUploadDays|analyzeTrends|RestClient|WebClient|HttpClient|ExecutorService' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 11566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## RestClientConfig\n'
sed -n '1,120p' src/main/java/com/example/inflace/global/config/RestClientConfig.java

printf '\n## YoutubeDataApiClient\n'
sed -n '1,260p' src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java

printf '\n## YoutubeSearchApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java

printf '\n## GoogleApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/GoogleApiClient.java

printf '\n## any timeout-related config files\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'setReadTimeout|setConnectTimeout|readTimeout|connectTimeout|timeout-duration|timeout\(' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 18032


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## RestClientConfig\n'
sed -n '1,120p' src/main/java/com/example/inflace/global/config/RestClientConfig.java

printf '\n## YoutubeDataApiClient\n'
sed -n '1,260p' src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java

printf '\n## YoutubeSearchApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java

printf '\n## GoogleApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/GoogleApiClient.java

printf '\n## timeout-related settings\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'setReadTimeout|setConnectTimeout|readTimeout|connectTimeout|timeout-duration|timeout\(' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 18024


computeAvgUploadDays에 응답 제한을 두세요.
CompletableFuture::join()RestClient.builder().build()로 만든 YouTube 호출을 그대로 기다립니다. application.ymltimeout-duration은 rate limiter 대기 제한이라 HTTP 응답 지연을 막지 못하므로, orTimeout()RestClient read timeout을 함께 넣어야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java`
around lines 275 - 294, `computeAvgUploadDays` is waiting indefinitely on
YouTube requests, so add an actual response timeout instead of relying on the
rate-limiter setting alone. Update the async flow around
`CompletableFuture.supplyAsync(...)` and `CompletableFuture::join` to use
`orTimeout()` (or equivalent) for the upload-day computation, and configure the
`RestClient` used by `computeUploadDays` with a read timeout so slow HTTP
responses fail fast. Keep the existing exception handling/logging in
`BrandCollaborationService` to surface timeout failures clearly.

Comment on lines +90 to +91
@Bulkhead(name = "youtube-data", fallbackMethod = "youtubeDataStringListFallback")
@RateLimiter(name = "youtube-data", fallbackMethod = "youtubeDataStringListFallback")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

RateLimiter가 내부 루프의 개별 API 호출을 정확히 제어하지 못합니다.

getMyVideoIds(라인 92)는 페이지네이션 while 루프에서 API를 여러 번 호출하지만, @RateLimiter는 메서드 호출당 1 permit만 소비합니다. getYoutubeVideos(라인 141)도 청크 루프에서 동일한 패턴입니다.

예시: 500개 영상이 있는 플레이리스트에 getMyVideoIds를 호출하면 10개 API 호출이 발생하지만 rate limiter에는 1 permit만 카운트됩니다. rate limit이 20/s일 때 실제 API 호출 속도는 200/s에 도달할 수 있어 YouTube Data API 할당량(10,000 units/day)을 50초 이내에 소진할 수 있습니다.

@Bulkhead는 동기 호출이므로 동시 HTTP 요청을 16개로 제한하는 데 효과적이지만, @RateLimiter는 버스트 속도를 실제 API 호출 단위가 아닌 메서드 호출 단위로만 제한합니다.

후속 작업에서 HTTP 클라이언트 레벨 rate limiting 또는 루프 내 호출당 rate limiter 적용을 검토하세요.

Also applies to: 139-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java`
around lines 90 - 91, The current `@RateLimiter` on YoutubeDataApiClient only
limits each method invocation once, so it does not control the individual API
requests made inside the pagination/chunk loops. Review getMyVideoIds and
getYoutubeVideos and move rate limiting to the actual YouTube API call points
inside those loops, or switch to HTTP-client-level throttling, while keeping
`@Bulkhead` on the outer methods for concurrency control. Ensure the
fallbackMethod wiring still works after refactoring and that each remote call is
counted against the limiter rather than the enclosing method.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/main/java/com/example/inflace/global/config/RestClientConfig.java (2)

17-17: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

SimpleClientHttpRequestFactory는 커넥션 풀링을 지원하지 않습니다.

SimpleClientHttpRequestFactory는 내부적으로 HttpURLConnection을 사용하여 요청마다 새 TCP 연결을 생성합니다. 이 PR에서 여러 외부 API 클라이언트가 Bulkhead 기반 가상 스레드 병렬 실행으로 동시 요청을 보내므로, 커넥션 재사용이 성능에 도움이 될 수 있습니다. 프로덕션 환경에서는 JdkClientHttpRequestFactory 또는 HttpComponentsClientHttpRequestFactory(Apache HttpClient 의존성 필요)로 전환을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/inflace/global/config/RestClientConfig.java` at
line 17, Update RestClientConfig to replace SimpleClientHttpRequestFactory with
a connection-pooling request factory, preferably JdkClientHttpRequestFactory or
HttpComponentsClientHttpRequestFactory, and configure the corresponding client
while preserving the existing RestClient behavior.

12-23: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

타임아웃 값을 application.yml로 외부화하는 것을 권장합니다.

이 PR은 Resilience4j Bulkhead/RateLimiter 설정을 application.yml에 외부화하면서 "운영 부하에 따라 제한 값을 조정할 수 있다"는 목표를 밝히고 있습니다. 하지만 RestClient의 connect/read 타임아웃은 상수로 하드코딩되어 있어, 운영 중 타임아웃 조정 시 재배포가 필요합니다. Resilience4j 설정과 일관성 있게 @Value 또는 @ConfigurationProperties로 주입하는 것이 좋습니다.

♻️ 제안: application.yml 기반 타임아웃 주입
+import org.springframework.beans.factory.annotation.Value;
+
 `@Configuration`
 public class RestClientConfig {
 
-    private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(2);
-    private static final Duration READ_TIMEOUT = Duration.ofSeconds(5);
+    `@Value`("${rest-client.connect-timeout:2s}")
+    private Duration connectTimeout;
+
+    `@Value`("${rest-client.read-timeout:5s}")
+    private Duration readTimeout;
 
     `@Bean`
     public RestClient restClient() {
         SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
-        requestFactory.setConnectTimeout(CONNECT_TIMEOUT);
-        requestFactory.setReadTimeout(READ_TIMEOUT);
+        requestFactory.setConnectTimeout(connectTimeout);
+        requestFactory.setReadTimeout(readTimeout);
 
         return RestClient.builder()
                 .requestFactory(requestFactory)
                 .build();
     }
 }

application.yml에 추가:

rest-client:
  connect-timeout: 2s
  read-timeout: 5s
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/inflace/global/config/RestClientConfig.java` around
lines 12 - 23, Update RestClientConfig.restClient to read connect and read
timeout values from application.yml instead of the hardcoded CONNECT_TIMEOUT and
READ_TIMEOUT constants, using `@Value` or a dedicated `@ConfigurationProperties`
binding for the rest-client settings. Preserve the existing
SimpleClientHttpRequestFactory configuration while allowing both durations to be
changed through external configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/main/java/com/example/inflace/global/config/RestClientConfig.java`:
- Line 17: Update RestClientConfig to replace SimpleClientHttpRequestFactory
with a connection-pooling request factory, preferably
JdkClientHttpRequestFactory or HttpComponentsClientHttpRequestFactory, and
configure the corresponding client while preserving the existing RestClient
behavior.
- Around line 12-23: Update RestClientConfig.restClient to read connect and read
timeout values from application.yml instead of the hardcoded CONNECT_TIMEOUT and
READ_TIMEOUT constants, using `@Value` or a dedicated `@ConfigurationProperties`
binding for the rest-client settings. Preserve the existing
SimpleClientHttpRequestFactory configuration while allowing both durations to be
changed through external configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81daf1ab-650e-4b8e-b277-0d4846f4454f

📥 Commits

Reviewing files that changed from the base of the PR and between 4abca99 and a88d289.

📒 Files selected for processing (6)
  • build.gradle
  • src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java
  • src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java
  • src/main/java/com/example/inflace/global/config/RestClientConfig.java
  • src/main/resources/application.yml
  • src/test/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • build.gradle
  • src/main/resources/application.yml
  • src/test/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchServiceTest.java
  • src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java
  • src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java

@HanYoonSoo
HanYoonSoo merged commit 9cb3816 into develop Jul 13, 2026
3 checks passed
@HanYoonSoo
HanYoonSoo deleted the in-370-외부-api-호출-bulkhead-패턴-사용 branch July 13, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants